package com.danicsoft.daide.serverclient;

import java.io.IOException;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import com.danicsoft.daide.binary.ClientConnection;
import com.danicsoft.daide.binary.Connection;
import com.danicsoft.daide.command.AdminMessageCommand;
import com.danicsoft.daide.command.DefaultCommandTarget;
import com.danicsoft.daide.command.GoFlagCommand;
import com.danicsoft.daide.command.HelloCommand;
import com.danicsoft.daide.command.HistoryCommand;
import com.danicsoft.daide.command.HuhCommand;
import com.danicsoft.daide.command.IamCommand;
import com.danicsoft.daide.command.MapCommand;
import com.danicsoft.daide.command.MapDefinitionCommand;
import com.danicsoft.daide.command.MissingCommand;
import com.danicsoft.daide.command.NameCommand;
import com.danicsoft.daide.command.NotCommand;
import com.danicsoft.daide.command.NowCommand;
import com.danicsoft.daide.command.ObserverCommand;
import com.danicsoft.daide.command.OrderCommand;
import com.danicsoft.daide.command.OrderSpec;
import com.danicsoft.daide.command.ParenthesisCommand;
import com.danicsoft.daide.command.PissOffCommand;
import com.danicsoft.daide.command.SaveCommand;
import com.danicsoft.daide.command.SoloCommand;
import com.danicsoft.daide.command.SubmitCommand;
import com.danicsoft.daide.command.SupplyOwnershipCommand;
import com.danicsoft.daide.command.ThanksCommand;
import com.danicsoft.daide.command.TimeCommand;
import com.danicsoft.daide.command.Turn;
import com.danicsoft.daide.historian.ServerHistorian;
import com.danicsoft.daide.token.TokenMap;
import com.danicsoft.daide.token.TokenParser;
import com.danicsoft.daide.token.TokenMap.Phases;

/**
 * This class sits on the client side and interacts with the server. For a
 * client to interact with the server, they must create a new ServerProxy and
 * <code>join()</code> it. If the client is running on the same computer as
 * the server it could simply join a Server there.
 * 
 * @author Daniel Yule (daniel.yule@gmail.com)
 * @version 1.0
 */
public class ServerProxy extends DefaultCommandTarget implements Server {

	/**
	 * Create a new Server proxy to sit in front of the server at the specified
	 * address and port.
	 * 
	 * @param address
	 *        The address of the server.
	 * @param port
	 *        The port of the server.
	 * @throws IOException If we could not connect.
	 */
	public ServerProxy(InetAddress address, int port) throws IOException {
		this(address, port, new ServerHistorian());

	}

	/**
	 * Create a new Server proxy to sit in front of the server at the specified
	 * address and port, logging with the given historian.
	 * 
	 * @param address
	 *        The address of the server.
	 * @param port
	 *        The port of the server.
	 * @param historian
	 *        The historian we will use to log messages.
	 * @throws IOException If we could not connect.
	 */
	public ServerProxy(InetAddress address, int port, ServerHistorian historian) throws IOException {
		this(new ClientConnection(address, port, historian), historian);
		this.historian = historian;

	}
	
	/**
	 * Invoke the super's constructor and connect.
	 * @param conn The connection we are connecting on.
	 * @param myParser The TokenParser we are using to parse;
	 * @throws IOException If we could not connect.
	 */
	private ServerProxy(Connection conn, TokenParser myParser) throws IOException {
		super(conn, myParser);
		conn.connect(myParser);
	}

	/**
	 * Pass the construction up the chain with a newly added TokenParser
	 * 
	 * @param conn
	 *        The connection we are contructing from.
	 * @param historian
	 *        The historian we are using.
	 * @throws IOException If we could not connect.
	 */
	private ServerProxy(Connection conn, ServerHistorian historian) throws IOException {
		this(conn, new TokenParser(new TokenMap(), conn, historian));
		
		
	}

	/**
	 * The player that this is a proxy for.
	 */
	private Player myPlayer;

	/**
	 * The historian we will use for logging all of the updates that occur.
	 */
	private ServerHistorian historian;

	/**
	 * The variant information for this game.
	 */
	private HelloCommand vInfo;

	/**
	 * The last SCO message we have recieved.
	 */
	private SupplyOwnershipCommand currentSCO;

	/**
	 * The last NOW message we have recieved.
	 */
	private NowCommand currentNOW;

	/**
	 * The most recent SCO command we have recieved
	 */
	private SupplyOwnershipCommand recentSCO;

	/**
	 * The name of the map this game is playing.
	 */
	private String mapName;

	/**
	 * The state that the server is currently in.
	 */
	private SERVER_STATE state = SERVER_STATE.WAITING_FOR_PLAYERS;

	/**
	 * The current list of orders we have recieved.
	 */
	private List<OrderCommand> currentOrders = new ArrayList<OrderCommand>();

	/**
	 * The current turn;
	 */
	private Turn thisTurn;

	/**
	 * A set of the orders that we have sent this turn.
	 * 
	 * @see #thanksReceived
	 */
	private Set<OrderSpec> ordersSent = new HashSet<OrderSpec>();

	/**
	 * A set of thanks commands for the orders we have sent this turn.
	 * 
	 * @see #ordersSent
	 */
	private Set<ThanksCommand> thanksReceived;

	/**
	 * The Map definition for this game.
	 */
	private MapDefinitionCommand mapDef;

	/**
	 * Check to see if we have recieved all the information we need to start the
	 * game, and if so inform the player that it has started.
	 */
	private void checkStartGame() {
		if (state == SERVER_STATE.WAITING_FOR_PLAYERS && vInfo != null) {
			TurnInfo tInfo = new TurnInfo();
			tInfo.armyPosition = currentNOW;
			tInfo.scOwnership = currentSCO;
			tInfo.turn = currentNOW.getTurn().clone();
			thisTurn = tInfo.turn;
			myPlayer.startGame(vInfo.power, vInfo.passcode, vInfo, tInfo);
		}
	}

	/**
	 * Displays an error message indicating that the server's state is invalid.
	 */
	private void invalidState() {
		String stateString = "";
		switch (state) {
			case GAME_ENDED:
				stateString = "game has ended.";
				break;
			case GAME_PAUSED:
				stateString = "game is paused.";
				break;
			case GAME_STARTED:
				stateString = "game is in progress.";
				break;
			case WAITING_FOR_PLAYERS:
				stateString = "game has not started yet";
		}
		historian.logErrorMessage(this, "Could not complete request because " + stateString);
	}

	@Override
	protected void AcceptedIam(IamCommand command) {
		sendCommand(new MapDefinitionCommand());
		state = SERVER_STATE.GAME_STARTED;
		historian.logUpdateMessage(this, "Reconnect accepted");

	}

	@Override
	protected void AcceptedName(NameCommand command) {
		historian.logUpdateMessage(this, "Connection accepted, waiting for map");
	}

	@Override
	protected void AcceptedNot(NotCommand command) {
	// TODO Parse the NOT command here.

	}

	@Override
	protected void AcceptedObserver(ObserverCommand command) {
		historian.logUpdateMessage(this, "Accepted Observer connection, waiting for map.");
		myPlayer.accept();
	}

	@Override
	protected void AcceptedTime(TimeCommand command) {
	// TODO Automatically generated

	}

	@Override
	protected void ReceivedAdminMessage(AdminMessageCommand command) {
		myPlayer.adminMessage(command.getName(), command.getMessage());

	}

	@Override
	protected void ReceivedHello(HelloCommand command) {
		vInfo = command;
	}

	@Override
	protected void ReceivedHuh(HuhCommand command) {
		historian.logErrorMessage(this, "The server could not parse a message.");

	}

	@Override
	protected void ReceivedMap(MapCommand command) {
		mapName = command.mapName;
		sendCommand(new MapDefinitionCommand());

	}

	@Override
	protected void ReceivedMapDefintion(MapDefinitionCommand command) {
		mapDef = command;
		try {
			myPlayer.notifyMapInfo(getMapInfo());
		}
		catch (IllegalServerStateException e) {}

	}

	@Override
	protected void ReceivedMissing(MissingCommand command) {
		if (command.isPositions()) {
			if (thisTurn.season.getData() == Phases.FAL || thisTurn.season.getData() == Phases.SUM) {
				myPlayer.notifyOfMissingRetreats(command.getMissingPositions());
			}
			else {
				myPlayer.notifyOfMissingOrders(command.getMissingPositions());
			}
		}
		else {
			myPlayer.notifyOfMissingRemovals(command.getRemovalsLeft());
		}

	}

	@Override
	protected void ReceivedNot(NotCommand command) {
	// TODO Parse the various meanings of not, although I don't think a client
	// ever recieves a straight NOT command.

	}

	@Override
	protected void ReceivedNow(NowCommand command) {
		if (thisTurn == null) {
			currentNOW = command;
			checkStartGame();
		}
		else if (command.getTurn().compareTo(thisTurn) < 0) {
			logThisTurn();
			currentNOW = command;
			if (recentSCO != null) {
				currentSCO = recentSCO;
			}
			
			try {
				myPlayer.nextTurn(getCurrentTurnInfo());
				ordersSent.clear();
			}
			catch (IllegalServerStateException e) {
				
			}
		}

	}

	/**
	 * Logs the information for this turn and prepare for the next one.
	 */
	private void logThisTurn() {

		try {
			historian.logTurn(thisTurn, getCurrentTurnInfo());
		}
		catch (IllegalServerStateException e) {

		}

	}

	@Override
	protected void ReceivedOff(PissOffCommand command) {
		historian.logUpdateMessage(this, "Server asked us to disconnect.");
		sever();

	}

	@Override
	protected void ReceivedParenthesis(ParenthesisCommand command) {
		historian.logErrorMessage(this, "Sent a command with mismatched parenthesis.");

	}

	@Override
	protected void ReceivedSave(SaveCommand command) {
	// TODO Automatically generated

	}

	@Override
	protected void ReceivedSupplyOwnership(SupplyOwnershipCommand command) {
		currentSCO = command;

	}

	@Override
	protected void ReceivedThanks(ThanksCommand command) {
		//thanksReceived.add(command);
		myPlayer.acceptOrder(command.getOrder(), command.getNote());

	}

	@Override
	protected void ReceivedTime(TimeCommand command) {
		myPlayer.notifyUpcomingDeadline(command.getTimeLeft());

	}

	@Override
	protected void RejectedAdminMessage(AdminMessageCommand command) {
		historian.logErrorMessage(this, "Server has disabled admin messages.");

	}

	@Override
	protected void RejectedGoFlag(GoFlagCommand command) {
		invalidState();

	}

	@Override
	protected void RejectedHello(HelloCommand command) {
		invalidState();

	}

	@Override
	protected void RejectedHistory(HistoryCommand command) {
		invalidState();

	}

	@Override
	protected void RejectedIam(IamCommand command) {
		historian.logErrorMessage(this, "Could not reconnect as " + command.power.toString() + ".  Incorrect credentials.");

	}

	@Override
	protected void RejectedName(NameCommand command) {
		historian.logErrorMessage(this, "Could not connect as " + command.name);

	}

	@Override
	protected void RejectedNot(NotCommand command) {
	// TODO Automatically generated

	}

	@Override
	protected void RejectedNow(NowCommand command) {
		invalidState();

	}

	@Override
	protected void RejectedObserver(ObserverCommand command) {
		historian.logErrorMessage(this, "Could not connect as an observer");

	}

	@Override
	protected void RejectedOrder(OrderCommand command) {
		invalidState();

	}

	@Override
	protected void RejectedSubmit(SubmitCommand command) {
		invalidState();

	}

	@Override
	protected void RejectedSupplyOwnership(SupplyOwnershipCommand command) {
		invalidState();

	}

	@Override
	protected void RejectedTime(TimeCommand command) {
		if (command.getTimeLeft() >= 0) {
			historian.logErrorMessage(this, "Notification time longer than any deadline.");
		}
		else {
			historian.logErrorMessage(this, "Could not request negative notification time.");
		}

	}

	public TurnInfo getCurrentTurnInfo() throws IllegalServerStateException {
		TurnInfo tInfo = new TurnInfo();
		tInfo.armyPosition = currentNOW;
		tInfo.scOwnership = currentSCO;
		tInfo.orders = currentOrders;
		tInfo.turn = currentNOW.getTurn();
		return tInfo;
	}

	public MapInfo getMapInfo() throws IllegalServerStateException {
		MapInfo mInfo = new MapInfo();
		mInfo.name = mapName;
		mInfo.defintion = mapDef;
		return mInfo;
	}

	public VariantInfo getVariantInfo() throws IllegalServerStateException {
		return vInfo;
	}

	public boolean join(Player player) {
		if (state == SERVER_STATE.WAITING_FOR_PLAYERS) {
			myPlayer = player;
			sendCommand(new NameCommand(player.getName(), player.getVersion()));
			return true;
		}
		else {
			return false;
		}
	}

	public void left(Player leftPlayer) {
		disconnect();

	}

	public boolean observe(Player player) {
		if (state != SERVER_STATE.GAME_ENDED) {
			myPlayer = player;
			sendCommand(new ObserverCommand());
			return true;
		}
		else {
			return false;
		}
	}

	public boolean ready(Player player) {
		if (state == SERVER_STATE.WAITING_FOR_PLAYERS) {
			acceptCommand(new MapCommand(mapName));
			return true;
		}
		return false;
	}

	public boolean rejoin(Player player) {
		sendCommand(new IamCommand(myPlayer.getPasscode(), myPlayer.getPower()));
		return true;
	}

	public void submitOrders(List<OrderSpec> orders, Player player) throws IllegalServerStateException {
		ordersSent.addAll(orders);
		sendCommand(new SubmitCommand(orders));

	}

	public boolean unsubmitOrder(List<OrderSpec> orders, Player player) {
		if (ordersSent.removeAll(orders)) {
			sendCommand(new NotCommand(new SubmitCommand(orders)));
			return true;
		}
		return false;
	}

	public void unsubmitOrders(Player player) throws IllegalServerStateException {
		ordersSent.clear();
		sendCommand(new NotCommand(new SubmitCommand()));

	}

	@Override
	public String toString() {
		return "Remote Server";
	}


	@Override
	protected void ReceivedOrder(OrderCommand command) {
		currentOrders.add(command);
	}

	/**
	 * 
	 * @param command
	 * @see com.danicsoft.daide.command.DefaultCommandTarget#ReceivedSolo(com.danicsoft.daide.command.SoloCommand)
	 */
	@Override
	protected void ReceivedSolo(SoloCommand command) {
		//TODO Automatically generated
		super.ReceivedSolo(command);
	}

}
